Python Classes in an Interview

Everything you need to define a small class under time pressure, using the Google car rental question as the running example. Every snippet below was run before being written down. Ordered by what you'll reach for first.

The question this is built around

Car Rental Request Scheduling Google phone screen ยท asked Aug 5 and Aug 10, 2026 ยท both candidates passed

You are given N available cars and M rental requests. Each request is a tuple (pickupTime, returnTime, id):

pickupTimewhen the customer picks up the car
returnTimewhen the customer returns the car
ida unique request identifier

A car may serve multiple requests as long as their time intervals do not overlap. If one request returns a car at time t and another picks up a car at exactly time t, they may use the same car.

Assign a car to every request while satisfying all of the following:

1Output the assigned car ID for every request.
2Minimise the number of cars used.
3Design a Car class that stores a car ID and its assigned orders.
4Car IDs start at 1 and are assigned in the order cars are first created.
Follow-up (part of the original question, not an extra): there are at most K available cars. If all requests can be served with at most K cars, output the assignment. Otherwise output IMPOSSIBLE. Reported constraints: 1 <= n <= 200,000, 1 <= K <= n, 0 <= start < end <= 10^9, all IDs distinct.

Example 1 โ€” feasible

# input: n=4 orders, K=2 cars
#        id  start  end
        101     1    4
        102     2    5
        103     4    6
        104     5    7

# output
1: 101 103
2: 102 104
Read the boundary. Order 101 returns at 4 and order 103 picks up at 4, so they share car 1. Same for 102 returning at 5 and 104 picking up at 5. That single detail is the <= versus < decision in your heap check, and it's the most likely place to lose the round quietly.

Example 2 โ€” infeasible

# input: n=3 orders, K=1 car
#        id  start  end
          1     1    3
          2     2    4
          3     4    5

# output
IMPOSSIBLE
Orders 1 and 2 overlap on [2, 3), so two cars are needed at minimum. With K=1 there is no valid assignment. Note that orders 1 and 3 would share a car happily โ€” the infeasibility comes from a single overlapping pair, not from the whole set.

What's actually being tested

The algorithm is Meeting Rooms II (#253) with an output requirement bolted on. If you've done #253, the scheduling half is solved.

The class requirement is the real content. "Design a Car class that stores a car ID and its assigned orders" is an unusual instruction for a Google coding round, and it forces three things at once โ€” a per-instance mutable list, an ordering so the objects go straight into heapq, and a separate stable-order list for output because the heap reorders itself. That's the whole reason this cheatsheet exists.

Clarifying questions worth asking before you code: can two requests share a car when one ends exactly as the other begins? (Yes โ€” but make them say it.) Is the input sorted? Are IDs guaranteed distinct? Should output be ordered by car ID or by request?

The 30-second version

The whole thing

class Car:
    def __init__(self, car_id: int):
        self.id = car_id            # fields are just assignments in __init__
        self.orders: List[int] = []   # fresh list per instance
        self.free_at = 0

    def assign(self, order_id: int, end: int) -> None:
        self.orders.append(order_id)
        self.free_at = end

car = Car(1)
car.assign(101, 4)

That is a complete, interview-acceptable class. No constructor keyword, no new, no field declarations above __init__. Fields spring into existence when you assign to self.something.

self is not magic and not optional. It's the instance, passed as the first argument automatically. You write it in every method signature; you never pass it at the call site. Forgetting it is the single most common syntax error under pressure.

Answers to the questions you're actually asking

Do I need a constructor?Only if the object has state to initialise. __init__ is the constructor. If there's nothing to set, skip it entirely.
Do I declare fields first?No. Assign them in __init__. Type hints on the assignment are optional and read as senior.
Public / private?No enforcement in Python. A leading underscore _free_at is a convention meaning "internal." Don't bother in a 45-minute round.
Getters and setters?No. Access the attribute directly. Writing Java-style getters in Python is a mild negative signal.
Return type of __init__?None. It mutates self; it does not return the object.
Inheritance?Almost never needed in a coding round. If asked: class SUV(Car): then super().__init__(car_id) inside its __init__.

The one trap that will actually bite you

Mutable class attribute โ€” shared across every instance

# WRONG โ€” orders is defined on the CLASS, so all cars share one list
class Car:
    orders = []                     # class attribute
    def __init__(self, car_id):
        self.id = car_id

a, b = Car(1), Car(2)
a.orders.append('x')
print(b.orders)                     # ['x']  <-- b was never touched
# RIGHT โ€” orders is created per instance inside __init__
class Car:
    def __init__(self, car_id):
        self.id = car_id
        self.orders = []            # instance attribute

I ran the wrong version above and it really does print ['x']. This is the bug that silently produces a correct-looking answer with wrong output, and it's very hard to spot while an interviewer is watching.

Same trap in function signatures: def f(items=[]) reuses one list across every call. Use def f(items=None) then items = items or [].
Worth saying out loud: "I'm putting orders in __init__ rather than at class level so each car gets its own list."

Making your class work with heapq

__lt__ โ€” the only comparison a heap needs

The car rental question needs a min-heap of cars ordered by next-free time. heapq compares elements with <, so give the class a __lt__ and you can push the objects directly.

def __lt__(self, other: "Car") -> bool:
    return self.free_at < other.free_at

Two alternatives, both fine, and worth naming so the interviewer sees you know the options:

# 1. push a tuple, no __lt__ needed
heapq.heappush(heap, (car.free_at, car.id, car))
#   include a tiebreaker (car.id) so Python never has to
#   compare two Car objects when free_at ties -> TypeError

# 2. dataclass with order=True (see below)
The tuple tiebreaker matters. Without car.id in the middle, two equal free_at values make Python fall through to comparing the Car objects, and you get TypeError: '<' not supported. This is a classic live-interview crash.

__repr__ โ€” free debugging

def __repr__(self) -> str:
    return f"Car(id={self.id}, free_at={self.free_at}, orders={self.orders})"

Without it, print(car) gives <__main__.Car object at 0x104f2a3d0>, which is useless when you're dry-running by hand in a Google Doc. With it, printing a list of cars shows you the whole state at a glance.

Cheap credibility: add __repr__ early and say "this makes the dry run readable." Interviewers notice.

The full car rental solution

Verified against both example cases from the real question

import heapq
from typing import List, Optional, Tuple

class Car:
    def __init__(self, car_id: int):
        self.id = car_id
        self.orders: List[int] = []
        self.free_at = 0

    def assign(self, order_id: int, end: int) -> None:
        self.orders.append(order_id)
        self.free_at = end

    def __lt__(self, other: "Car") -> bool:
        return self.free_at < other.free_at

    def __repr__(self) -> str:
        return f"Car(id={self.id}, free_at={self.free_at}, orders={self.orders})"


def assign_cars(orders: List[Tuple[int, int, int]],
                k: Optional[int] = None) -> Optional[List[Car]]:
    """orders = [(id, start, end)]. Returns cars, or None if k is too small."""
    orders = sorted(orders, key=lambda o: o[1])   # by pickup time
    heap: List[Car] = []          # cars in use, ordered by free_at
    cars: List[Car] = []          # every car, in creation order

    for oid, start, end in orders:
        if heap and heap[0].free_at <= start:   # <= : return at t, pickup at t is OK
            car = heapq.heappop(heap)
        else:
            if k is not None and len(cars) >= k:
                return None                  # IMPOSSIBLE
            car = Car(len(cars) + 1)
            cars.append(car)
        car.assign(oid, end)
        heapq.heappush(heap, car)

    return cars
Why heap and cars are separate lists: the heap reorders itself constantly, so it can't give you cars in creation order for the output. cars holds stable insertion order. Both hold references to the same objects, so car.assign(...) is visible through either โ€” that's Python's reference semantics doing the work for you, and it's worth saying out loud.

Complexity: O(n log n) for the sort plus O(n log n) heap operations. O(n) space.

How to test it in the room

Bare assert, no framework

You have no test runner in a Google Doc or CoderPad. Plain asserts read as deliberate and cost three lines.

# example 1 from the problem: 4 orders, 2 cars
res = assign_cars([(101,1,4), (102,2,5), (103,4,6), (104,5,7)], k=2)
assert [c.orders for c in res] == [[101,103], [102,104]]

# example 2: infeasible with 1 car
assert assign_cars([(1,1,3), (2,2,4), (3,4,5)], k=1) is None

# edge cases worth naming even if you don't code them
assert assign_cars([]) == []                        # empty input
assert len(assign_cars([(1,1,2), (2,2,3)])) == 1   # touching intervals share a car
The touching-intervals assert is the valuable one. The problem states a return at time t and a pickup at time t may share a car. That's the difference between <= and < in your heap check, and it's exactly the kind of off-by-one an interviewer probes. Testing it proves you read the spec.

Test list to state out loud

Empty inputNo orders โ†’ no cars, not a crash.
Single orderExactly one car.
All overlappingn orders all at once โ†’ n cars. Worst case.
None overlappingSequential orders โ†’ 1 car. Best case.
Touching at a boundaryEnd == next start. The <= vs < decision.
k too small / k = nIMPOSSIBLE path, and the trivially-feasible path.
Unsorted inputYour sort handles it, but say that you rely on it.

Dataclasses โ€” when they help, when they don't

The short version

from dataclasses import dataclass, field

@dataclass(order=True)
class Car:
    free_at: int = 0
    id: int = 0
    orders: List[int] = field(
        default_factory=list, compare=False)

You get __init__, __repr__, __eq__ and (with order=True) all the comparisons for free, generated in field declaration order.

Two things to know

1. Field order is comparison order. free_at is listed first so heap ordering works. Reorder the fields and you silently change the sort key.

2. Mutable defaults need default_factory. Writing orders: List[int] = [] raises ValueError at class definition โ€” the dataclass module catches the shared-mutable trap for you. compare=False keeps the list out of comparisons.

Should you use one on Aug 20?

Probably not. A plain class is three more lines and shows the interviewer you know what __init__ and __lt__ actually do. A dataclass hides exactly the mechanics being assessed when someone says "design a Car class."

Best of both: write the plain class, then mention "in production I'd make this a dataclass โ€” I'm writing it out to keep the ordering behaviour explicit."

Dict keys and set members

__eq__ + __hash__ โ€” only when you need it

By default, two objects with identical fields are different keys, because the default hash is identity-based. If you need to dedupe objects in a set or use them as dict keys, define both.

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __eq__(self, o):
        return isinstance(o, Point) and (self.x, self.y) == (o.x, o.y)
    def __hash__(self):
        return hash((self.x, self.y))

len({Point(1,2), Point(1,2), Point(3,4)})   # 2
Defining __eq__ alone sets __hash__ to None and your object becomes unhashable. Always define both or neither.

For the 2D clustering question, skip all of this โ€” index into the points list and union find on integer indices. Objects as dict keys is more machinery than that problem needs.

Syntax reference

You wantWrite
Constructordef __init__(self, a, b):
Instance fieldself.name = value inside __init__
Methoddef method(self, arg): โ€” self always first
Printabledef __repr__(self): return f"..."
Heap-sortabledef __lt__(self, other): return ...
Set / dict key__eq__ and __hash__, always both
Length via len()def __len__(self): return len(self.orders)
Shared across instancesClass attribute โ€” only for immutables like MAX = 100
No instance needed@staticmethod (no self) or @classmethod (takes cls)
Alternate constructor@classmethod
def from_tuple(cls, t): return cls(*t)
Inheritanceclass SUV(Car): then super().__init__(car_id)
Forward type referencedef __lt__(self, other: "Car") โ€” quotes, class isn't defined yet

Mistakes ranked by how likely you are to make them at 11:15am

1Forgetting self in a method signature.
2Mutable class attribute shared across instances.
3Pushing tuples to a heap without a tiebreaker โ†’ TypeError on ties.
4__eq__ without __hash__ โ†’ object silently unhashable.
5Calling Car.assign(order) on the class instead of an instance.
6Expecting __init__ to return the object. It returns None.
7Writing Java-style get_id() / set_id(). Just use the attribute.
Every snippet here was executed before being written down. Companion to google-prep.html โ€” the car rental question is real and was asked twice in August 2026.